Skip to content

feat(desktop,host-service): tombstone workspace deletes + kanban board view for the workspaces page - #6296

Merged
Kitenite merged 6 commits into
mainfrom
research-kanban-workspace
Aug 9, 2026
Merged

Kitenite merged 6 commits into
mainfrom
research-kanban-workspace

Conversation

@Kitenite

@Kitenite Kitenite commented Aug 9, 2026 •

Copy link
Copy Markdown
Member

Links

  • ExecPlan: plans/done/20260809-1015-workspace-tombstones-kanban-board.md (decision log + deviations recorded there)

Summary

This PR bundles two related features, decided in a design walkthrough with Kiet:

  1. Workspace tombstoning — deleting a workspace soft-deletes its host.db row (archivedAt + archiveReason: "merged" | "deleted") instead of erasing it. Sessions tombstone too (reason is always "deleted" — they have no PR link).
  2. Kanban board view — the workspaces page gains a Linear-style board layout behind a list ↔ board toggle, with Merged/Deleted history columns fed by the tombstones, plus URL-synced multi-select filters shared by both layouts.

Part 1: Tombstoning (host-service)

Why

Merged and deleted workspaces vanished without a trace — the row delete was documented as "the commit point and the only record." History is the raw material for the board's Merged/Deleted columns.

What / How

  • workspaces gains archivedAt/archiveReason; pullRequests gains a sticky mergedAt (migration 0020, additive only, generated with drizzle-kit — not hand-edited).
  • The destroy pipeline archives at a new commit point after the teardown script (step 1.5): reason is "merged" when the linked PR was observed merged, else "deleted". Any later cleanup failure un-archives so the workspace stays visible and retryable; the archive broadcast reuses the existing deleted event shape so no consumer needs a wire migration.
  • runArchivedWorkspaceReconcile (startup sweep) finishes crash-interrupted deletes — archived rows whose worktree still exists — with force + best-effort teardown, guarded so it never touches a path a live row owns (worktree paths get reused when a branch is re-created).
  • workspace.list excludes archived rows by default; includeArchived: true opts in. Adopt-existing-worktree, branch search, worktree re-import, name backfill, the git watcher, and all PR refresh sweeps skip tombstones; getPullRequestsByWorkspaces deliberately still serves archived ids so ghost cards keep their frozen PR pill.
  • Retention: keep forever (visibility is bounded by the board's archived window instead).

Key Decisions

Decision Choice Rationale
Storage Soft-delete in place, consumers updated Single source of truth; no unique-index collisions exist (worktreePath/branch are unindexed; one-main-per-project can't archive)
Commit point After teardown, before physical cleanup Blocking teardown failure re-opens the delete dialog for force-retry; archiving first would unmount it mid-prompt
Sessions Tombstone too (originally exempt; reversed in review) "Essentially workspaces with a little missing data" — simplifies the pipeline and gives sessions Deleted-column history. Folder-name claiming counts tombstones, so paths never reuse
Retention Keep forever Rows are small metadata; "Show archived" window bounds the UI

Part 2: Board + filters (desktop)

Why

The page was a flat table with a substring search and two single-select dropdowns — no way to see work by state. Linear's board-is-a-layout model (researched against Linear's docs/Mobbin) fits: columns are derived state, filters are layout-independent, done items stay accessible without cluttering.

What / How

  • Data layer first: the page now keeps the host's full 5-valued PR state (incl. queued) instead of discarding it and re-deriving a worse 4-valued state from the cloud Electric githubPullRequests join, which is removed (local-first; also fixes sessions never matching). Live agent status comes from one host-wide terminalAgents.list query per host — never per workspace. Archived tombstones ride a dedicated query key in useHostWorkspacesSource so the shared live cache and its IndexedDB snapshots never see them.
  • Board: fixed workflow-ordered columns — Idle · Working · Needs attention · Needs review · Merged · Deleted — derived in a unit-tested pure util (deriveBoardColumn, first match wins: archived reason → live merged PR → permission/failed → working → review/open PR → idle). Archived cards render ghosted with a reason badge and are non-clickable. No drag-and-drop in v1 (columns are derived state; nothing settable to drag to). "Show archived: none / past week / past month / all" (default past week) scopes tombstones.
  • Filters: validateSearch on the route — q, device, projects (multi, __sessions__ sentinel), pr, agent, view, archived. URL hydrates the store once per mount, then the store drives the URL (replace). Copying the URL reproduces the exact view, including board mode.
  • DeletingWorkspacesProvider deleted: the durable archive mark replaces the renderer-memory in-flight set (which other devices never saw anyway). The v2 row's delete dialog stays mounted via a mounted-once-opened latch so the teardown-failure force-retry still re-opens.

Manual QA Checklist (all verified via CDP on the dev app from this worktree, screenshots captured)

Tombstoning

  • Destroy through the real pipeline archives with reason deleted; worktree removed, row kept (verified in host.db)
  • Destroy of a workspace linked to a merged PR archives with reason merged
  • Migration 0020 applied cleanly to an existing dev host.db on boot
  • Archived rows absent from the list view and sidebar; live board gains the ghost card without a refresh (event-driven invalidation)

Board / filters

  • List ↔ board toggle; URL carries ?view=board
  • All six columns render with correct counts against real data (82 workspaces)
  • PR-state multi-select filters the board; URL becomes ?pr=merged&view=board
  • Full location.reload() on a filtered URL restores board mode + active filters from the URL alone
  • queued PR state renders (amber) end-to-end now that host state is used

Not manually exercised (covered by tests / low-risk reasoning)

  • Blocking-teardown failure un-archive (integration-tested; not reproduced in the live app)
  • Startup reconciler resume (unit-tested selection logic; destroy path is the same code the live QA exercised)
  • Remote-host board (single dev host available)

Testing

  • bun run typecheck — clean, all packages
  • bun run lint — exits 0
  • packages/host-service: bun test — 1072 pass / 0 fail (new: archive semantics integration tests, merged-reason test, session tombstone tests, reconciler stranded-selection tests, commit-point failure tests)
  • apps/desktop: bun test — 2630 pass / 0 fail (new: deriveBoardColumn precedence tests)

Known Limitations

  • The PR hover card no longer shows +/- line counts or "updated X ago" — those were cloud-only fields; the host store doesn't track diff stats. Can return if the host PR fetch starts carrying them.
  • During the teardown-script window a workspace mid-delete looks normal in lists (no dimmed row anymore); it vanishes at the archive commit point seconds later. A second delete attempt gets the existing in-progress CONFLICT toast.
  • Removing a project cascades away that project's tombstones (existing FK behavior, judged acceptable).

Follow-ups (deliberately out of scope)

  • Drag-and-drop column semantics; board group-by picker (columns are fixed to status)
  • Time-windowed "merged in last N days" for live merged workspaces using the new mergedAt
  • Saved views / filter pills with operators (Linear-style)

Rollout / Rollback

  • Migration is additive (two nullable columns + index, one nullable column); old builds ignore the new columns. Forward-only, no backfill needed.
  • Rollback: revert the code; existing tombstone rows are inert under old code except they reappear as live workspaces in lists (old list has no archived filter) with missing worktrees — acceptable for a short rollback window, or clear with delete from workspaces where archived_at is not null.

Summary by cubic

Soft-delete all workspaces (including sessions) in packages/host-service and add a Kanban board view with URL‑synced filters in apps/desktop. This update also tightens safety and UX: archived rows are fetched only for the board, navigation ignores in‑flight deletes, and the reconciler adds extra guards.

  • New Features

    • Tombstoning: archive workspaces with archivedAt/archiveReason ("merged" or "deleted"); sessions also tombstone ("deleted"); PRs get sticky mergedAt.
    • Safe cleanup: archive after teardown; failures un‑archive; startup reconciler finishes crash‑interrupted deletes and re‑checks live path ownership; refresh/update paths guard on archivedAt IS NULL; project removal sweeps stranded tombstone worktrees; deletion telemetry fires at the true commit.
    • API/UI: workspace.list excludes tombstones by default (includeArchived: true to opt in); creation/search/backfill/watchers skip archived; tombstones fetched under a separate query key; archived polling runs only in board view; archived rows report honest hostReachable.
    • Board view: fixed columns (Idle, Working, Needs attention, Needs review, Merged, Deleted); archived cards render ghosted with reason; shared workspace context menu reused by list rows and board cards (ghosts have no actions).
    • Filters: multi‑select projects (incl. sessions), PR states (incl. queued), agent statuses, device, archived window; all URL‑synced with list↔board toggle; URL hydration applies only present params.
    • Data: PR state and agent status sourced from the host; shared checks dot indicator; navigation/shortcuts exclude mid‑teardown rows via a module‑scope deletingWorkspaces store; removed DeletingWorkspacesProvider.
  • Migration

    • Apply DB migration 0020 adding workspaces.archived_at, workspaces.archive_reason, and pull_requests.merged_at (additive, forward‑only; no backfill).

Written for commit 773df8b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a Kanban board view for workspaces with workflow-based columns.
    • Added list/board toggles, archived-workspace viewing, and URL-synchronized filters.
    • Added multi-select filters for projects, pull-request states, and agent status.
    • Added queued pull-request status indicators and richer workspace cards.
    • Added shared workspace actions for opening, pinning, copying branches, and deleting.
  • Bug Fixes
    • Workspace deletion now preserves recoverable archive records and improves cleanup retry behavior.
    • Archived workspaces are excluded from active workspace operations by default.

Deleting a workspace now archives its row (archivedAt + archiveReason
merged|deleted) at a commit point right after the teardown script; any
later cleanup failure un-archives so the workspace stays retryable, and
a startup reconciler finishes crash-interrupted deletes (guarded against
worktree-path reuse). Sessions keep today's hard delete. workspace.list
excludes archived rows unless the caller opts in; adopt/search/backfill/
watcher/PR sweeps all skip tombstones. pullRequests gains a sticky
mergedAt stamped at first merged observation (migration 0020).
The workspaces page now keeps the host's full 5-valued PR state (incl.
merge queue) instead of discarding it and re-deriving a 4-valued state
from the cloud Electric join, which is removed; live agent status comes
from one host-wide terminalAgents.list query per host. Archived
tombstones ride a dedicated query key in useHostWorkspacesSource so the
shared live cache and snapshots never see them. DeletingWorkspacesProvider
is deleted — the durable archive mark replaces the in-memory in-flight
set, and the delete dialog stays mounted via a per-row latch so the
teardown-failure force-retry still re-opens.
…page

Board/list toggle on the workspaces page: fixed status columns (Idle,
Working, Needs attention, Needs review, Merged, Deleted) derived from
agent status + PR state, ghosted archived cards, and a "Show archived"
window dropdown (default past week). Filters become URL-synced: search,
multi-select projects (with a Sessions sentinel), PR states, agent
statuses, device, view mode, archived window — copy the URL, get the view.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds workspace tombstones, archived-workspace retrieval, host-derived PR and agent status, a Kanban board, URL-synchronized filters, shared workspace actions, and Zustand-based deletion state.

Changes

Workspace tombstones and board

Layer / File(s) Summary
Host archive lifecycle
packages/host-service/..., packages/host-service/test/..., packages/host-service/drizzle/...
Workspace deletion now archives rows with merge or delete reasons. Failed cleanup can unarchive rows. Startup reconciliation handles stranded worktrees. Active queries exclude archived rows.
Archived workspace query contract
packages/host-service/src/trpc/router/workspace/workspace.ts, apps/desktop/src/renderer/hooks/host-workspaces/...
Workspace listing supports optional archived rows. The renderer uses a separate archived query and cache path.
Desktop workspace data and deletion state
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/..., apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/..., apps/desktop/src/renderer/routes/_authenticated/layout.tsx
Workspace data uses host PR snapshots and terminal-agent status. Archive metadata flows into accessible workspaces. Deletion tracking moves from a provider to a Zustand store.
Workspace board and status derivation
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/..., .../V2WorkspaceContextMenu/..., .../V2WorkspaceRow/...
The dashboard adds ordered board columns, archived cards, shared workspace actions, queued PR presentation, and check indicators.
Workspace filters and routing
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx, .../V2WorkspacesHeader/..., .../v2WorkspacesFilterStore/...
The route supports multi-select project, PR-state, and agent-status filters, archived windows, list or board views, and URL synchronization.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant V2WorkspacesPage
  participant AccessibleWorkspaces
  participant HostWorkspaceSource
  participant WorkspaceBoard
  User->>V2WorkspacesPage: Select filters or board view
  V2WorkspacesPage->>AccessibleWorkspaces: Request filtered workspaces
  AccessibleWorkspaces->>HostWorkspaceSource: Load live and archived snapshots
  HostWorkspaceSource-->>AccessibleWorkspaces: Return workspace, PR, and archive data
  AccessibleWorkspaces-->>V2WorkspacesPage: Return accessible workspaces
  V2WorkspacesPage->>WorkspaceBoard: Render grouped workspace cards
Loading

Possibly related PRs

Suggested reviewers: saddlepaddle

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title uses conventional commit format and clearly identifies both primary changes: workspace tombstoning and the Kanban board view.
Description check ✅ Passed The description is detailed and covers the changes, rationale, testing, limitations, rollout, and follow-ups required by the template.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch research-kanban-workspace

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx (1)

69-71: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unmount the dialog after a canceled delete.

Line 71 latches deleteDialogMounted permanently. A normal close calls onOpenChange(false), but it never clears the latch. Each opened-and-canceled dialog then keeps its live-query subscription for the lifetime of the list.

Clear the latch after an ordinary close. Keep it mounted only while a teardown operation can reopen the dialog.

Also applies to: 374-386

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx`
around lines 69 - 71, Update the V2WorkspaceRow delete-dialog lifecycle so
deleteDialogMounted is cleared when onOpenChange receives a normal false close,
releasing the dialog and its live-query subscription after cancellation.
Preserve the latch only while teardown is in progress and may need to reopen the
dialog, including the existing force-delete recovery path.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx (1)

6-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move ChecksDot into its own component file.

ChecksDot is a second React component in this file. Move it to components/ChecksDot/ChecksDot.tsx, add its index.ts barrel export, and import it here.

As per coding guidelines, “Use one component per file and organize components as ComponentName/ComponentName.tsx with an index.ts barrel export.”

Also applies to: 115-128

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`
around lines 6 - 12, Move the ChecksDot component out of
V2WorkspacesBoardCard.tsx into components/ChecksDot/ChecksDot.tsx, add the
corresponding index.ts barrel export, and import ChecksDot from that barrel in
V2WorkspacesBoardCard.tsx while preserving its current behavior and props.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.ts`:
- Around line 275-283: Update the archived data mapping in useHostWorkspaces so
hostReachable reflects the corresponding archivedQueries[index] error state
instead of always being true. Reuse the same reachability derivation used by the
live-query path, while preserving the existing archived row filtering and
mapping behavior.
- Around line 173-196: Update the isReady calculation in useHostWorkspaces to
include settlement of every enabled query in archivedQueries when
includeArchived is true. Treat disabled archived queries as already settled,
preserve rendering of any live or archived data already available, and keep the
existing readiness behavior when archived data is not requested.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts`:
- Around line 61-69: Update useDashboardSidebarShortcuts and its callers to
receive the in-flight deletion workspace IDs, then filter those IDs out of
flattenedWorkspaces before generating shortcut targets. Ensure
JUMP_TO_WORKSPACE_*, PREV_WORKSPACE, and NEXT_WORKSPACE cannot navigate to
workspaces pending deletion.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx`:
- Around line 95-143: Update the synchronization around hydratedRef and the
useEffect in the v2 workspaces route so changes to Route.useSearch() reapply all
URL filter values to useV2WorkspacesFilterStore before the store-to-URL
navigation can overwrite them, while preserving initial hydration behavior. Add
a route test covering same-route navigation between /v2-workspaces URLs with
different filters and verify the store and resulting URL remain synchronized.

In `@packages/host-service/src/runtime/archived-workspace-reconcile.ts`:
- Around line 35-44: Update the stranded-workspace teardown flow around
selectStranded and destroyWorkspace to revalidate ownership of each
row.worktreePath in the same serialized critical section as teardown. Prevent
force deletion when a live workspace claims the path between the initial
livePaths snapshot and cleanup, preferably by using an atomic path claim or a
cleanup guard inside destroyWorkspace; abort cleanup for paths owned by another
live row.

In `@packages/host-service/src/runtime/pull-requests/pull-requests.ts`:
- Around line 484-486: Update syncWorkspaceRow so its workspace update predicate
requires archivedAt IS NULL, preventing writes to archived tombstones even when
archival races with readWorkspaceRefs. Only refresh the project after the
conditional update reports success, and add a race test that archives the
workspace while readWorkspaceRefs is blocked.

In `@packages/host-service/src/trpc/router/project/project.ts`:
- Around line 842-853: Update the local workspace cleanup query in the project
deletion flow to include archived rows so their existing worktrees are removed
before the project cascade; remove the isNull(workspaces.archivedAt) filter
while preserving the projectId restriction. Add a test covering an archived
workspace with a still-existing worktree and verify it is cleaned up before
deletion.

In
`@packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts`:
- Around line 462-469: Update the phase label in the comment above the local
commit remainder logic near deleteLocalWorkspace to reference step 1.5 instead
of step 0.5, matching the archive phase documentation elsewhere.
- Around line 313-323: Update the rollback condition around marked,
archiveLocalWorkspace, and unarchiveLocalWorkspace so rollback occurs only when
the workspace row was live before this attempt. Capture whether local.archivedAt
was unset before archiving, use that state to gate the catch-block unarchive,
and preserve existing behavior for newly archived rows while never clearing a
pre-existing tombstone.

In `@packages/host-service/test/archived-workspace-reconcile.test.ts`:
- Around line 1-2: Move the unit test from the archived workspace reconciliation
test location to sit beside the source file containing selectStranded. Preserve
the existing test coverage and imports, adjusting only relative paths as needed
so the co-located test directly exercises selectStranded.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`:
- Around line 6-12: Move the ChecksDot component out of
V2WorkspacesBoardCard.tsx into components/ChecksDot/ChecksDot.tsx, add the
corresponding index.ts barrel export, and import ChecksDot from that barrel in
V2WorkspacesBoardCard.tsx while preserving its current behavior and props.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx`:
- Around line 69-71: Update the V2WorkspaceRow delete-dialog lifecycle so
deleteDialogMounted is cleared when onOpenChange receives a normal false close,
releasing the dialog and its live-query subscription after cancellation.
Preserve the latch only while teardown is in progress and may need to reopen the
dialog, including the existing force-delete recovery path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 51509868-6f51-46e4-bbb9-b0310dd88375

📥 Commits

Reviewing files that changed from the base of the PR and between f231665 and f2564ac.

📒 Files selected for processing (56)
  • apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.ts
  • apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.utils.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarBulkDeleteDialog/hooks/useBulkWorkspaceDelete/useBulkWorkspaceDelete.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacePrHoverCardContent/V2WorkspacePrHoverCardContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacePrHoverCardContent/components/PrStateBadge/PrStateBadge.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/V2WorkspacesBoard.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardColumn/V2WorkspacesBoardColumn.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardColumn/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/utils/deriveBoardColumn/deriveBoardColumn.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/utils/deriveBoardColumn/deriveBoardColumn.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/utils/deriveBoardColumn/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/MultiSelectFilter.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/ProjectFilterTriggerLabel.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/V2WorkspacesList.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/stores/v2WorkspacesFilterStore/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/stores/v2WorkspacesFilterStore/v2WorkspacesFilterStore.ts
  • apps/desktop/src/renderer/routes/_authenticated/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/DeletingWorkspacesProvider.tsx
  • apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/index.ts
  • packages/host-service/drizzle/0020_overrated_dreaming_celestial.sql
  • packages/host-service/drizzle/meta/0020_snapshot.json
  • packages/host-service/drizzle/meta/_journal.json
  • packages/host-service/src/app.ts
  • packages/host-service/src/db/schema.ts
  • packages/host-service/src/events/git-watcher.ts
  • packages/host-service/src/runtime/archived-workspace-reconcile.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • packages/host-service/src/runtime/workspace-backfill.ts
  • packages/host-service/src/trpc/router/project/project.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
  • packages/host-service/src/trpc/router/workspace-creation/procedures/list-project-worktrees.ts
  • packages/host-service/src/trpc/router/workspace-creation/procedures/search-branches.ts
  • packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts
  • packages/host-service/src/trpc/router/workspace/workspace.ts
  • packages/host-service/src/workspaces/local-workspace-store.ts
  • packages/host-service/test/archived-workspace-reconcile.test.ts
  • packages/host-service/test/integration/bug-hunt-2.integration.test.ts
  • packages/host-service/test/integration/workspace-cleanup.integration.test.ts
  • packages/host-service/test/integration/workspace-create-delete.integration.test.ts
  • packages/host-service/test/integration/workspace.integration.test.ts
  • packages/host-service/test/migration-0019-preserves-terminal-links.test.ts
  • packages/host-service/test/workspace-cleanup.test.ts
  • plans/done/20260809-1015-workspace-tombstones-kanban-board.md
💤 Files with no reviewable changes (6)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/ProjectFilterTriggerLabel.tsx
  • apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarBulkDeleteDialog/hooks/useBulkWorkspaceDelete/useBulkWorkspaceDelete.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/DeletingWorkspacesProvider.tsx

Comment on lines +1 to +2
import { describe, expect, test } from "bun:test";
import { selectStranded } from "../src/runtime/archived-workspace-reconcile";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Co-locate this unit test with selectStranded.

Move this test beside packages/host-service/src/runtime/archived-workspace-reconcile.ts. This test exercises one utility and is not a cross-cutting invariant test.

As per coding guidelines, “Co-locate tests with the component, hook, or utility they test.” Based on learnings, “follow the AGENTS.md rule to co-locate per-file unit tests (.test.ts) next to the source they test.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/test/archived-workspace-reconcile.test.ts` around lines
1 - 2, Move the unit test from the archived workspace reconciliation test
location to sit beside the source file containing selectStranded. Preserve the
existing test coverage and imports, adjusting only relative paths as needed so
the co-located test directly exercises selectStranded.

Sources: Coding guidelines, Learnings

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 56 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts">

<violation number="1" location="packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts:123">
P1: Recreating a branch after deletion can return its archived workspace as an existing workspace, skipping adoption/creation. Filter tombstones from `findExistingWorkspaceByBranch` so this new live-only lookup behavior remains consistent through the earlier idempotency checks.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx:378">
P3: Cancelling a delete permanently mounts this row’s delete-dialog hook tree; opening and cancelling dialogs across a large workspace list accumulates hidden components that rerender with shared host/preference context. Preserve the latch only while a destroy is in flight or a teardown failure must be recoverable, then clear it on an ordinary close.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsx:199">
P3: An active project filter loses its visible label when the search query excludes that project's options, so the trigger incorrectly says “All projects” although filtering remains active. Preserve selected projects from `projectsById` in the options/trigger label independent of search results.</violation>
</file>

<file name="packages/host-service/src/app.ts">

<violation number="1" location="packages/host-service/src/app.ts:258">
P0: A workspace recreated after the reconcile path snapshot can have its worktree deleted: the reconciler selects an archived row, creation registers a live row at that path, then the queued destroy removes that shared path. Revalidate live-path ownership atomically/immediately before each destroy (or serialize creation and reconciliation by path).</violation>
</file>

<file name="packages/host-service/src/runtime/pull-requests/pull-requests.ts">

<violation number="1" location="packages/host-service/src/runtime/pull-requests/pull-requests.ts:432">
P2: The periodic sweeps materialize every permanent tombstone before filtering, so refresh cost grows with deletion history. Filter `archived_at IS NULL` in SQL and update the test fakes to support the predicate.</violation>
</file>

<file name="packages/host-service/src/runtime/archived-workspace-reconcile.ts">

<violation number="1" location="packages/host-service/src/runtime/archived-workspace-reconcile.ts:26">
P1: A live workspace using a symlink-equivalent path can be treated as stranded and deleted at startup because `livePaths` compares raw strings while cleanup canonicalizes paths. Normalize both archived and live paths before ownership comparison.</violation>

<violation number="2" location="packages/host-service/src/runtime/archived-workspace-reconcile.ts:35">
P1: A user-created worktree reusing a permanent tombstone's path is deleted on the next host restart before it can be imported, since DB ownership is the only safety check. Reconcile only a checkout provably belonging to the interrupted delete, using a durable deletion marker/identity rather than path existence alone.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Desktop Renderer
    participant HostSvc as Host Service (tRPC)
    participant HostDB as Host SQLite
    participant Bus as Event Bus
    participant Recon as Startup Reconcile

    Note over Client,HostSvc: Workspace Delete Flow (tombstoning)
    Client->>HostSvc: destroy(workspaceId, options)
    HostSvc->>HostSvc: preflight + run teardown script
    alt non-session workspace
        HostSvc->>HostDB: NEW: archive row (archivedAt, reason)
        HostSvc->>HostSvc: physical cleanup (PTY, worktree, branch, cloud)
        alt cleanup fails
            HostSvc->>HostDB: NEW: unarchive row (revive)
            HostSvc-->>Client: error
        else cleanup succeeds
            HostSvc-->>Client: success
        end
        HostSvc->>Bus: NEW: broadcast "deleted" (same shape as before)
    else session workspace
        HostSvc->>HostDB: hard delete (unchanged)
        HostSvc->>Bus: broadcast "deleted"
    end

    Note over Recon,HostDB: Startup Reconcile (crash recovery)
    Recon->>HostDB: select archived rows
    Recon->>HostDB: select live worktree paths
    Recon->>Recon: filter stranded (worktree exists + unowned)
    loop each stranded row
        Recon->>HostSvc: destroy(force=true, teardown=best-effort)
    end

    Note over Client,HostSvc: Board View Data Flow
    Client->>HostSvc: workspace.list({includeArchived:true})
    HostSvc->>HostDB: select live rows (default excludes archived)
    HostSvc->>HostDB: select archived rows (opt-in)
    HostSvc-->>Client: rows with archivedAt/reason + live data
    Client->>HostSvc: terminalAgents.list (host-wide)
    HostSvc-->>Client: agent statuses per terminal
    Client->>HostSvc: getPullRequestsByWorkspaces (host PR snapshot)
    HostSvc-->>Client: PR state (5-valued incl. queued), mergedAt
    Client->>Client: deriveBoardColumn() → assign to column (NEW)
    Note over Client: ghosted archived cards render with reason badge
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

});
// Finish any delete the previous process crashed out of (archived row
// whose worktree still exists).
await runArchivedWorkspaceReconcile({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: A workspace recreated after the reconcile path snapshot can have its worktree deleted: the reconciler selects an archived row, creation registers a live row at that path, then the queued destroy removes that shared path. Revalidate live-path ownership atomically/immediately before each destroy (or serialize creation and reconciliation by path).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/app.ts, line 258:

<comment>A workspace recreated after the reconcile path snapshot can have its worktree deleted: the reconciler selects an archived row, creation registers a live row at that path, then the queued destroy removes that shared path. Revalidate live-path ownership atomically/immediately before each destroy (or serialize creation and reconciliation by path).</comment>

<file context>
@@ -252,6 +253,23 @@ export function createApp(options: CreateAppOptions): CreateAppResult {
 		});
+		// Finish any delete the previous process crashed out of (archived row
+		// whose worktree still exists).
+		await runArchivedWorkspaceReconcile({
+			git,
+			credentials: providers.credentials,
</file context>

where: and(
eq(workspaces.projectId, projectId),
eq(workspaces.branch, branch),
isNull(workspaces.archivedAt),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Recreating a branch after deletion can return its archived workspace as an existing workspace, skipping adoption/creation. Filter tombstones from findExistingWorkspaceByBranch so this new live-only lookup behavior remains consistent through the earlier idempotency checks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts, line 123:

<comment>Recreating a branch after deletion can return its archived workspace as an existing workspace, skipping adoption/creation. Filter tombstones from `findExistingWorkspaceByBranch` so this new live-only lookup behavior remains consistent through the earlier idempotency checks.</comment>

<file context>
@@ -120,6 +120,7 @@ export async function adoptExistingWorktree(
 			where: and(
 				eq(workspaces.projectId, projectId),
 				eq(workspaces.branch, branch),
+				isNull(workspaces.archivedAt),
 			),
 		})
</file context>

.all();
if (archived.length === 0) return;

const livePaths = new Set(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A live workspace using a symlink-equivalent path can be treated as stranded and deleted at startup because livePaths compares raw strings while cleanup canonicalizes paths. Normalize both archived and live paths before ownership comparison.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/runtime/archived-workspace-reconcile.ts, line 26:

<comment>A live workspace using a symlink-equivalent path can be treated as stranded and deleted at startup because `livePaths` compares raw strings while cleanup canonicalizes paths. Normalize both archived and live paths before ownership comparison.</comment>

<file context>
@@ -0,0 +1,73 @@
+		.all();
+	if (archived.length === 0) return;
+
+	const livePaths = new Set(
+		ctx.db
+			.select({ worktreePath: workspaces.worktreePath })
</file context>

.map((row) => row.worktreePath),
);

const stranded = selectStranded(archived, livePaths, existsSync);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A user-created worktree reusing a permanent tombstone's path is deleted on the next host restart before it can be imported, since DB ownership is the only safety check. Reconcile only a checkout provably belonging to the interrupted delete, using a durable deletion marker/identity rather than path existence alone.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/runtime/archived-workspace-reconcile.ts, line 35:

<comment>A user-created worktree reusing a permanent tombstone's path is deleted on the next host restart before it can be imported, since DB ownership is the only safety check. Reconcile only a checkout provably belonging to the interrupted delete, using a durable deletion marker/identity rather than path existence alone.</comment>

<file context>
@@ -0,0 +1,73 @@
+			.map((row) => row.worktreePath),
+	);
+
+	const stranded = selectStranded(archived, livePaths, existsSync);
+
+	for (const row of stranded) {
</file context>

only once the user opened it — not idle for every row. The latch
keeps it mounted through the destroy so a teardown-failure can
re-open it to offer force-delete. */}
{!isMainWorkspace && deleteDialogMounted ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Cancelling a delete permanently mounts this row’s delete-dialog hook tree; opening and cancelling dialogs across a large workspace list accumulates hidden components that rerender with shared host/preference context. Preserve the latch only while a destroy is in flight or a teardown failure must be recoverable, then clear it on an ordinary close.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx, line 378:

<comment>Cancelling a delete permanently mounts this row’s delete-dialog hook tree; opening and cancelling dialogs across a large workspace list accumulates hidden components that rerender with shared host/preference context. Preserve the latch only while a destroy is in flight or a teardown failure must be recoverable, then clear it on an ordinary close.</comment>

<file context>
@@ -366,11 +371,11 @@ export function V2WorkspaceRow({
+			    only once the user opened it — not idle for every row. The latch
+			    keeps it mounted through the destroy so a teardown-failure can
+			    re-open it to offer force-delete. */}
+			{!isMainWorkspace && deleteDialogMounted ? (
 				<DashboardSidebarDeleteDialog
 					workspaceId={workspace.id}
</file context>

</SelectGroup>
<MultiSelectFilter
placeholder="All projects"
options={projectFilterOptions}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: An active project filter loses its visible label when the search query excludes that project's options, so the trigger incorrectly says “All projects” although filtering remains active. Preserve selected projects from projectsById in the options/trigger label independent of search results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsx, line 199:

<comment>An active project filter loses its visible label when the search query excludes that project's options, so the trigger incorrectly says “All projects” although filtering remains active. Preserve selected projects from `projectsById` in the options/trigger label independent of search results.</comment>

<file context>
@@ -133,62 +194,43 @@ export function V2WorkspacesHeader({
-							</SelectGroup>
+					<MultiSelectFilter
+						placeholder="All projects"
+						options={projectFilterOptions}
+						value={projectFilters}
+						onChange={setProjectFilters}
</file context>

Sessions are workspaces with a little missing data — no reason to exempt
them from delete history. The pipeline loses its per-type branch at the
commit point (reason is always "deleted"; sessions have no PR link), and
sessions gain Deleted-column history on the board. Safe against path
reuse: claimedSessionNames counts tombstones, so a tombstone's folder
name is never handed to a new session and the reconciler's live-path
guard holds. deleteLocalWorkspace remains only for adopt-conflict
phantom-row cleanup.
@Kitenite

Kitenite commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Update: sessions now tombstone too (2c9e7eb). Original design exempted sessions (hard delete); reversed after review — they're essentially workspaces with a little missing data. Reason is always deleted (no PR link), the pipeline loses its per-type branch at the commit point, and sessions gain Deleted-column history. Path-reuse stays safe: claimedSessionNames counts tombstones, so a tombstone's folder name is never handed to a new session and the reconciler's live-path guard holds.

Delete-flow verification (CDP, real UI clicks on the dev app):

  • Happy path: row → trash → confirm dialog → toast → row vanished live; host.db row archived · deleted; worktree removed from disk; ghost card in the board's Deleted column.
  • Blocking teardown failure (clean workspace, project-level teardown.sh exiting 1): plain "Delete" ran the script, it failed, and the dialog re-opened with "Teardown exited with code 1" + output tail + "Delete anyway" — through the new mounted-once-opened latch (the deleted provider's job). At that moment host.db showed archived_at IS NULL: the commit point after teardown means a failed teardown never tombstones. "Delete anyway" then completed and archived.
  • Warnings path: existing behavior surfaced worth knowing — with warnings (e.g. unpushed commits), "Delete anyway" confirms with force=true → teardownMode: "skip", so the teardown script intentionally does not run (Teardown scripts are skipped when a workspace is deleted via MCP/CLI/SDK (force: true is hardcoded) #6174's consent semantics; unchanged by this PR).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/host-service/test/workspace-cleanup.test.ts (1)

708-708: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Co-locate this unit test with the workspace cleanup router.

packages/host-service/test/workspace-cleanup.test.ts tests one router module but is stored in the package-level test directory. Move it beside packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts. Keep integration tests under packages/host-service/test/integration/.

As per coding guidelines, tests must be co-located with the code they use. Based on learnings, host-service per-file unit tests are co-located, while cross-cutting integration tests remain under test/integration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/test/workspace-cleanup.test.ts` at line 708, Move the
unit tests from workspace-cleanup.test.ts beside the workspace-cleanup router
implementation, preserving their contents and test behavior. Keep only
cross-cutting integration tests under the host-service test/integration
directory.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/host-service/test/workspace-cleanup.test.ts`:
- Line 708: Move the unit tests from workspace-cleanup.test.ts beside the
workspace-cleanup router implementation, preserving their contents and test
behavior. Keep only cross-cutting integration tests under the host-service
test/integration directory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 86cc56e8-2762-4b05-bc3b-08a233bfb79f

📥 Commits

Reviewing files that changed from the base of the PR and between f2564ac and 2c9e7eb.

📒 Files selected for processing (7)
  • packages/host-service/src/db/schema.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
  • packages/host-service/src/trpc/router/workspace-creation/procedures/create-session.ts
  • packages/host-service/src/workspaces/local-workspace-store.ts
  • packages/host-service/test/integration/session-create-delete.integration.test.ts
  • packages/host-service/test/workspace-cleanup.test.ts
  • plans/done/20260809-1015-workspace-tombstones-kanban-board.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/host-service/src/workspaces/local-workspace-store.ts
  • packages/host-service/src/db/schema.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts">

<violation number="1" location="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:315">
P2: A corrupt session path outside the managed root now reports deletion success while leaving its folder and an archived, name-claiming tombstone; treat the skipped folder removal as a cleanup failure so the outer handler unarchives it for retry.</violation>
</file>

<file name="plans/done/20260809-1015-workspace-tombstones-kanban-board.md">

<violation number="1" location="plans/done/20260809-1015-workspace-tombstones-kanban-board.md:87">
P3: The new decision-log entry reverses the session exemption (sessions now tombstone on delete), but the rest of this plan still documents the opposite: Big Picture ("Sessions... are exempt and keep today's hard delete"), Milestone 2 steps 1/3 ("for non-session workspaces only" / "for sessions keep deleteLocalWorkspace (hard delete, unchanged)"), the Tests and Validation checklists ("session destroy still hard-deletes the row"), and Outcomes ("sessions still hard-delete"). A reader reconciling the reversed decision against the plan's operational steps will get conflicting instructions about the destroy pipeline. Please propagate the reversal to the affected sections so the plan is internally consistent.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// a little missing data (no project, no PRs; reason is always
// "deleted"), and session folder names are claimed against ALL rows
// including tombstones, so a tombstone's path can't be reused.
const marked = local != null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A corrupt session path outside the managed root now reports deletion success while leaving its folder and an archived, name-claiming tombstone; treat the skipped folder removal as a cleanup failure so the outer handler unarchives it for retry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts, line 315:

<comment>A corrupt session path outside the managed root now reports deletion success while leaving its folder and an archived, name-claiming tombstone; treat the skipped folder removal as a cleanup failure so the outer handler unarchives it for retry.</comment>

<file context>
@@ -309,8 +308,11 @@ async function runDestroy(
+	// a little missing data (no project, no PRs; reason is always
+	// "deleted"), and session folder names are claimed against ALL rows
+	// including tombstones, so a tombstone's path can't be reused.
+	const marked = local != null;
 	if (marked) {
 		archiveLocalWorkspace(ctx, input.workspaceId, archiveReasonFor(ctx, local));
</file context>

- Decision: `V2WorkspaceRow` keeps its delete dialog mounted via a "mounted once opened" latch instead of the deleted provider's `deleting` flag.
Rationale: preserves the teardown-failure re-open path without any global in-flight registry.
Date/Author: 2026-08-09, implementation.
- Decision: REVERSAL of the session exemption — sessions tombstone on delete too (reason always "deleted"; no PR link exists).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new decision-log entry reverses the session exemption (sessions now tombstone on delete), but the rest of this plan still documents the opposite: Big Picture ("Sessions... are exempt and keep today's hard delete"), Milestone 2 steps 1/3 ("for non-session workspaces only" / "for sessions keep deleteLocalWorkspace (hard delete, unchanged)"), the Tests and Validation checklists ("session destroy still hard-deletes the row"), and Outcomes ("sessions still hard-delete"). A reader reconciling the reversed decision against the plan's operational steps will get conflicting instructions about the destroy pipeline. Please propagate the reversal to the affected sections so the plan is internally consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plans/done/20260809-1015-workspace-tombstones-kanban-board.md, line 87:

<comment>The new decision-log entry reverses the session exemption (sessions now tombstone on delete), but the rest of this plan still documents the opposite: Big Picture ("Sessions... are exempt and keep today's hard delete"), Milestone 2 steps 1/3 ("for non-session workspaces only" / "for sessions keep deleteLocalWorkspace (hard delete, unchanged)"), the Tests and Validation checklists ("session destroy still hard-deletes the row"), and Outcomes ("sessions still hard-delete"). A reader reconciling the reversed decision against the plan's operational steps will get conflicting instructions about the destroy pipeline. Please propagate the reversal to the affected sections so the plan is internally consistent.</comment>

<file context>
@@ -84,6 +84,9 @@ All decisions below were made with Kiet in a walkthrough on 2026-08-09.
 - Decision: `V2WorkspaceRow` keeps its delete dialog mounted via a "mounted once opened" latch instead of the deleted provider's `deleting` flag.
   Rationale: preserves the teardown-failure re-open path without any global in-flight registry.
   Date/Author: 2026-08-09, implementation.
+- Decision: REVERSAL of the session exemption — sessions tombstone on delete too (reason always "deleted"; no PR link exists).
+  Rationale: Kiet, post-review: "they're essentially workspaces with a little missing data". Also simplifies the pipeline (no per-type branch at the commit point) and gives sessions Deleted-column history. Safe because `claimedSessionNames` counts tombstones, so a tombstone's folder path is never reused and the reconciler's live-path guard holds.
+  Date/Author: 2026-08-09, Kiet.
</file context>

Extract the list row's right-click menu (Open, Copy Branch Name,
Pin/Unpin, Delete) plus its delete-dialog latch into a shared
V2WorkspaceContextMenu render-prop component and wrap board cards in it.
The render prop hands the same actions to inline affordances, so the
card's click-to-open drops its duplicated paywall gating and the row's
pin/trash buttons reuse one implementation. Archived ghost cards render
without the menu — no actions apply to a tombstone.
@Kitenite

Kitenite commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Board cards now share the list row's right-click menu (2194546). Extracted the row's context menu + delete-dialog latch into V2WorkspaceContextMenu (render-prop; the actions feed the row's inline pin/trash buttons and the card's click-to-open, which drops its previously duplicated paywall gating). Archived ghost cards render without the menu — no actions apply to a tombstone.

CDP-verified live: identical menu items on row and card (Open · Copy Branch Name · Unpin from Sidebar · Delete), menu actions fire from a card, and a main workspace's card correctly omits Delete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx (1)

123-136: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add an accessible name for each check status.

ChecksDot renders status icons without text or an ARIA label. Add accessible labels for pending, successful, and failed checks. Render a visually hidden “No checks” label when status === "none" instead of returning an unlabeled absence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`
around lines 123 - 136, Update ChecksDot so every status has an accessible name:
render a visually hidden “No checks” label for “none”, and add visually hidden
labels identifying pending, successful, and failed checks alongside their icons.
Preserve the existing icon and styling for each non-none status.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx (1)

41-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the helper components into separate component files.

V2WorkspacesBoardCard.tsx defines V2WorkspacesBoardCard, BoardCardBody, and ChecksDot. Move each helper into its own ComponentName/ComponentName.tsx file and add the corresponding index.ts barrel export. Run bun run lint:fix, then verify that bun run lint exits successfully.

As per coding guidelines, use one component per file and organize components as ComponentName/ComponentName.tsx with an index.ts barrel export.

Also applies to: 123-136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`
around lines 41 - 50, Split V2WorkspacesBoardCard, BoardCardBody, and ChecksDot
into separate ComponentName/ComponentName.tsx files, preserving their existing
behavior and imports; add an index.ts barrel export for each component and
update consumers/imports accordingly. Run bun run lint:fix, then confirm bun run
lint succeeds.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`:
- Around line 123-136: Update ChecksDot so every status has an accessible name:
render a visually hidden “No checks” label for “none”, and add visually hidden
labels identifying pending, successful, and failed checks alongside their icons.
Preserve the existing icon and styling for each non-none status.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`:
- Around line 41-50: Split V2WorkspacesBoardCard, BoardCardBody, and ChecksDot
into separate ComponentName/ComponentName.tsx files, preserving their existing
behavior and imports; add an index.ts barrel export for each component and
update consumers/imports accordingly. Run bun run lint:fix, then confirm bun run
lint succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e41752af-d44c-48ab-b772-ca6ecc8d1fe9

📥 Commits

Reviewing files that changed from the base of the PR and between 2c9e7eb and 2194546.

📒 Files selected for processing (4)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspaceContextMenu/V2WorkspaceContextMenu.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspaceContextMenu/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx

Renderer: URL hydration now applies only params the URL carries, so a
bare sidebar navigation no longer resets board view and filters; the
empty-dashboard gate counts live rows, not tombstones; archived polling
runs only in board view; multi-select options key cmdk by unique id
(labels stay searchable via keywords); a module-scope
deletingWorkspaces store excludes mid-teardown workspaces from
navigation targets and keyboard shortcuts (the archive commit lands
after slow teardown, so rows stay listed for seconds).

Host: the reconciler re-checks live path ownership immediately before
each destroy; refresh-driven workspace updates carry archivedAt IS NULL
guards so an archive mid-await can't mutate a frozen tombstone;
project.remove sweeps stranded tombstone worktrees before the cascade
erases them; deletion telemetry fires at the true commit (failed
destroys don't count, retries count once); tombstones skip the
per-poll existsSync; archive-reason lookup failures log.

Also: archived-query settlement counts toward isReady, archived rows
report honest hostReachable, shared WorkspaceChecksDot, reconcile test
co-located, dead delete-throw test plumbing removed.
@Kitenite

Kitenite commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Review pass complete (773df8b). Ran a high-effort multi-agent review over the full diff plus triage of all 27 CodeRabbit/cubic comments.

Fixed:

  • Every-mount URL hydration wiped board view + filters on return navigation (now applies only params the URL carries)
  • Mid-teardown workspaces were targetable by navigate-away and keyboard shortcuts after the provider removal — replaced with a 30-line module-scope deletingWorkspacesStore (no provider tree), wired into both destroy flows + both target pickers
  • Empty-dashboard experiment gate counted tombstones (all.length → live rows)
  • cmdk multi-select items keyed by label collided on duplicate project names (→ unique id + keywords)
  • Reconciler snapshot race: live-path ownership re-checked immediately before each destroy (CodeRabbit + cubic P0)
  • Refresh-driven UPDATE workspaces sites now guard archivedAt IS NULL — an archive landing mid-await can't mutate a frozen tombstone
  • project.remove sweeps crash-stranded tombstone worktrees before the cascade erases them
  • Deletion telemetry moved to the true commit point (no double-count on retry, no counting failed destroys)
  • Archived rows skip the per-poll existsSync; archived polling is board-only; archived-query settlement counts toward isReady; honest hostReachable; shared WorkspaceChecksDot; dead test plumbing removed; reason-lookup failures log

Skipped with reasoning:

  • Agent-status 10s polling (matches this page's existing PR-poll pattern; event-driven invalidation is a follow-up)
  • mergedAt having no renderer consumer yet (deliberate — anchors the future "merged in last N days" window, per the plan's decision log)
  • Un-archive on post-archive retry failure resurrecting a tombstone (designed: visible-and-retryable beats invisible orphan disk state)
  • Corrupt-session-path unarchive-on-skip (would create a permanently undeletable workspace; archive+warn is strictly better)
  • Same-route search rehydration (no in-app parameterized links to this route; hydration is per-mount by design)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx (1)

184-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not replace an archived-only board with the create screen.

In board mode, all includes archived workspaces. If every live workspace was deleted, liveWorkspaces.length === 0 is true even when the board has archived ghost cards to render. The empty-screen experiment then hides the Deleted or Merged columns.

Use all.length === 0 for this condition, or exclude board mode from the create-screen path.

Proposed fix
-		liveWorkspaces.length === 0 &&
+		all.length === 0 &&
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx`
around lines 184 - 191, Update the isEmptyDashboard condition in the dashboard
page to use all.length === 0 instead of liveWorkspaces.length === 0, so
archived-only boards continue rendering their Deleted or Merged columns while
genuinely empty dashboards still show the create screen.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/host-service/src/trpc/router/project/project.ts`:
- Around line 843-853: Prevent duplicate workspace deletion telemetry during
project removal: in the cleanup loop handling localWorkspaces, ensure archived
rows with surviving worktrees are purged without invoking the telemetry-emitting
deleteLocalWorkspace path, or suppress telemetry when archivedAt is non-null
while preserving normal cleanup for live rows. Add a regression test covering
removal of a project containing an archived workspace whose worktree still
exists.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx`:
- Around line 184-191: Update the isEmptyDashboard condition in the dashboard
page to use all.length === 0 instead of liveWorkspaces.length === 0, so
archived-only boards continue rendering their Deleted or Merged columns while
genuinely empty dashboards still show the create screen.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 92055fdc-0bc9-4293-b158-05c44609c9ee

📥 Commits

Reviewing files that changed from the base of the PR and between 2194546 and 773df8b.

📒 Files selected for processing (22)
  • apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarBulkDeleteDialog/hooks/useBulkWorkspaceDelete/useBulkWorkspaceDelete.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/MultiSelectFilter.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/WorkspaceChecksDot/WorkspaceChecksDot.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/WorkspaceChecksDot/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx
  • packages/host-service/src/runtime/archived-workspace-reconcile.test.ts
  • packages/host-service/src/runtime/archived-workspace-reconcile.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • packages/host-service/src/trpc/router/project/project.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
  • packages/host-service/src/trpc/router/workspace/workspace.ts
  • packages/host-service/src/workspaces/local-workspace-store.ts
  • packages/host-service/test/workspace-cleanup.test.ts
  • plans/done/20260809-1015-workspace-tombstones-kanban-board.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/host-service/src/trpc/router/workspace/workspace.ts
  • packages/host-service/src/runtime/archived-workspace-reconcile.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx
  • apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/MultiSelectFilter.tsx
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • plans/done/20260809-1015-workspace-tombstones-kanban-board.md

Comment on lines +843 to +853
// The project-row delete below cascades tombstones away — removing a
// project intentionally drops its workspace history. Sweep worktrees
// for live rows AND stranded tombstones (crash-interrupted deletes
// whose worktree survives): once the cascade runs, the startup
// reconciler can no longer see them.
const localWorkspaces = ctx.db
.select()
.from(workspaces)
.where(eq(workspaces.projectId, input.projectId))
.all();
.all()
.filter((ws) => ws.archivedAt == null || existsSync(ws.worktreePath));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid duplicate deletion telemetry for archived rows.

This filter includes archived rows with existing worktrees. The later cleanup loop passes those rows to deleteLocalWorkspace at Line 871. That helper emits workspace_deleted telemetry at Line 216. The destroy workflow already calls trackWorkspaceDeleted after successful cleanup. An archived workspace with a surviving path can therefore produce two deletion events.

Use a no-telemetry purge for archived rows, or skip telemetry when the existing row has a non-null archivedAt. Add a regression test for project removal of an archived workspace with an existing worktree.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/host-service/src/trpc/router/project/project.ts` around lines 843 -
853, Prevent duplicate workspace deletion telemetry during project removal: in
the cleanup loop handling localWorkspaces, ensure archived rows with surviving
worktrees are purged without invoking the telemetry-emitting
deleteLocalWorkspace path, or suppress telemetry when archivedAt is non-null
while preserving normal cleanup for live rows. Add a regression test covering
removal of a project containing an archived workspace whose worktree still
exists.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 22 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.ts:12">
P2: Overlapping delete flows can clear the shared ID before teardown finishes, re-enabling shortcuts/navigation to a workspace still being destroyed. Track an in-flight count per ID (or ownership tokens) and remove it only when the final flow settles.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts:73">
P1: Prev/next shortcuts can still navigate into a workspace mid-destroy because the workspace page's concurrent handlers bypass this filtered list. Have those handlers consult `deletingIds` (or consolidate the bindings) so every shortcut target excludes in-flight deletes.</violation>
</file>

<file name="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts">

<violation number="1" location="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:329">
P2: A repeated destroy of an already archived workspace emits `workspace_deleted` again, contradicting the once-per-delete telemetry contract. Gate tracking on `local.archivedAt === null` from the pre-archive row so completed retries do not double-count.</violation>
</file>

<file name="packages/host-service/src/trpc/router/project/project.ts">

<violation number="1" location="packages/host-service/src/trpc/router/project/project.ts:853">
P2: Deleting a project leaves archived cards whose worktree is already gone visible on an open board for up to 30 seconds, because these rows are cascade-deleted without a `workspace:changed` event. Keep all project rows for the per-row `deleteLocalWorkspace` loop, while using a separate filtered set only for Git worktree cleanup.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx:126">
P1: Copied URLs that omit default filters do not reproduce the sender's view when the receiver has prior non-default store state; e.g. `?view=board` retains `archived=all` instead of the sender's default week. Encode defaults in shareable URLs or add an explicit navigation signal that resets omitted fields for deep links while preserving bare sidebar navigation.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

[groups, sessionWorkspaces, isDeleting],
// A destroy in flight keeps its row until the archive commit —
// don't hand shortcuts a workspace that is about to vanish.
].filter((workspace) => !deletingIds.has(workspace.id)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Prev/next shortcuts can still navigate into a workspace mid-destroy because the workspace page's concurrent handlers bypass this filtered list. Have those handlers consult deletingIds (or consolidate the bindings) so every shortcut target excludes in-flight deletes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts, line 73:

<comment>Prev/next shortcuts can still navigate into a workspace mid-destroy because the workspace page's concurrent handlers bypass this filtered list. Have those handlers consult `deletingIds` (or consolidate the bindings) so every shortcut target excludes in-flight deletes.</comment>

<file context>
@@ -58,15 +59,19 @@ export function useDashboardSidebarShortcuts(
+				),
+				// A destroy in flight keeps its row until the archive commit —
+				// don't hand shortcuts a workspace that is about to vanish.
+			].filter((workspace) => !deletingIds.has(workspace.id)),
+		[groups, sessionWorkspaces, deletingIds],
 	);
</file context>

...(search.view !== undefined && {
viewMode: "board" as V2WorkspacesViewMode,
}),
...(search.archived !== undefined && {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Copied URLs that omit default filters do not reproduce the sender's view when the receiver has prior non-default store state; e.g. ?view=board retains archived=all instead of the sender's default week. Encode defaults in shareable URLs or add an explicit navigation signal that resets omitted fields for deep links while preserving bare sidebar navigation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx, line 126:

<comment>Copied URLs that omit default filters do not reproduce the sender's view when the receiver has prior non-default store state; e.g. `?view=board` retains `archived=all` instead of the sender's default week. Encode defaults in shareable URLs or add an explicit navigation signal that resets omitted fields for deep links while preserving bare sidebar navigation.</comment>

<file context>
@@ -92,31 +92,45 @@ function V2WorkspacesPage() {
+			...(search.view !== undefined && {
+				viewMode: "board" as V2WorkspacesViewMode,
+			}),
+			...(search.archived !== undefined && {
+				archivedWindow: search.archived,
+			}),
</file context>

* by the destroy flows and read wherever a target is picked.
*/
interface DeletingWorkspacesState {
deletingIds: ReadonlySet<string>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Overlapping delete flows can clear the shared ID before teardown finishes, re-enabling shortcuts/navigation to a workspace still being destroyed. Track an in-flight count per ID (or ownership tokens) and remove it only when the final flow settles.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.ts, line 12:

<comment>Overlapping delete flows can clear the shared ID before teardown finishes, re-enabling shortcuts/navigation to a workspace still being destroyed. Track an in-flight count per ID (or ownership tokens) and remove it only when the final flow settles.</comment>

<file context>
@@ -0,0 +1,33 @@
+ * by the destroy flows and read wherever a target is picked.
+ */
+interface DeletingWorkspacesState {
+	deletingIds: ReadonlySet<string>;
+	markDeleting: (workspaceId: string) => void;
+	clearDeleting: (workspaceId: string) => void;
</file context>

});
// Telemetry at the true commit: a failed destroy un-archives below and
// must not count, and a retried destroy must count exactly once.
if (marked && local) trackWorkspaceDeleted(ctx, local);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A repeated destroy of an already archived workspace emits workspace_deleted again, contradicting the once-per-delete telemetry contract. Gate tracking on local.archivedAt === null from the pre-archive row so completed retries do not double-count.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts, line 329:

<comment>A repeated destroy of an already archived workspace emits `workspace_deleted` again, contradicting the once-per-delete telemetry contract. Gate tracking on `local.archivedAt === null` from the pre-archive row so completed retries do not double-count.</comment>

<file context>
@@ -318,7 +319,15 @@ async function runDestroy(
+		});
+		// Telemetry at the true commit: a failed destroy un-archives below and
+		// must not count, and a retried destroy must count exactly once.
+		if (marked && local) trackWorkspaceDeleted(ctx, local);
+		return result;
 	} catch (err) {
</file context>
Suggested change
if (marked && local) trackWorkspaceDeleted(ctx, local);
if (marked && local && local.archivedAt === null) trackWorkspaceDeleted(ctx, local);

.where(eq(workspaces.projectId, input.projectId))
.all();
.all()
.filter((ws) => ws.archivedAt == null || existsSync(ws.worktreePath));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Deleting a project leaves archived cards whose worktree is already gone visible on an open board for up to 30 seconds, because these rows are cascade-deleted without a workspace:changed event. Keep all project rows for the per-row deleteLocalWorkspace loop, while using a separate filtered set only for Git worktree cleanup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/project/project.ts, line 853:

<comment>Deleting a project leaves archived cards whose worktree is already gone visible on an open board for up to 30 seconds, because these rows are cascade-deleted without a `workspace:changed` event. Keep all project rows for the per-row `deleteLocalWorkspace` loop, while using a separate filtered set only for Git worktree cleanup.</comment>

<file context>
@@ -839,19 +840,17 @@ export const projectRouter = router({
-				.all();
+				.where(eq(workspaces.projectId, input.projectId))
+				.all()
+				.filter((ws) => ws.archivedAt == null || existsSync(ws.worktreePath));
 
 			for (const ws of localWorkspaces) {
</file context>

@Kitenite
Kitenite merged commit 9f056f7 into main Aug 9, 2026
19 checks passed
Kitenite added a commit that referenced this pull request Aug 12, 2026
* docs: weekly update for PRs merged 2026-08-05 to 2026-08-12

- workspaces: sessions (project-less workspaces, #6274), pin + drag-and-drop
  pinning (#6364), list/board views with Merged & Deleted history columns
  (#6296), per-project naming instructions (#6250), multi-PR links per
  branch with remove/restore (#5924)
- automations: session automations (#6280), list health stats + Up next
  grouping + read-only team automations (#6291, #6276), detail page
  Settings/Run History tabs, inline run errors, would-run preview, RRule
  validation (#6305)
- editor: image pane pan/pinch/zoom (#6285)
- tasks: multi-repo filters, expanded task states, PR author/review
  filters, PR checks (#6201)
- monorepos: per-project sparse checkout (#6034)
- customization: project icon + color picker, v1 color carry-over (#6249)
- terminal presets: built-in Superset CLI preset (#6270)
- agents: resume interrupted sessions + Resume Args setting (#6253, #6265),
  Superset hooks toggle + scoping (#6072)
- cli: interactive command browser + branded help (#6273); reference
  updated for session mode, --task, --skip-branch-prefix, --session
  (verified against packages/cli/src/commands)

* docs: tighten wording and drop em dashes

Shorten this week's additions per the writing guide: one thought per
sentence, plainest word, no em dashes as prose punctuation. Also sweeps
the three pre-existing em dashes in recipes.

* docs: add screenshots from the 08-09 changelog batch

cli command browser, agent resume banner, PR view with checks, and the
project icon/color picker. Copied into apps/docs/public/images per the
SOP rather than referenced across apps.
Kitenite added a commit that referenced this pull request Aug 19, 2026
…create (#6552)

* fix(host-service): don't reuse a tombstoned workspace on same-branch create

Since #6296 turned workspace deletes into tombstones (archivedAt), the
create flow's branch-idempotency lookup still matched archived rows, so
recreating a deleted workspace's name returned the dead row: no worktree
was created, the call reported alreadyExists, and the desktop navigated
to an id the workspace list filters out — landing on "Workspace not
found" (#6383, #6380).

Filter tombstones out of findExistingWorkspaceByBranch, matching the
isNull(archivedAt) convention every sibling lookup already follows, and
add an integration regression test for the delete → same-branch
recreate flow.

* docs(host-service): spell out the tombstone no-op symptom in the idempotency filter

Folds in the clearer rationale from the duplicate PR #6605: matching a
tombstone returns the archived row with alreadyExists: true and silently
skips the create, and the adopt path already filters archived rows.

---------

Co-authored-by: Kiet Ho <hoakiet98@gmail.com>

This branch was previously deployed

1 inactive deployment
preview — 773df8b5 Deployed Aug 9, 2026 by Kitenite via Deploy Docs #7338
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant